//	 Copyright 2011 Elian ORIOU
//
//   Licensed under the Apache License, Version 2.0 (the "License");
//   you may not use this file except in compliance with the License.
//   You may obtain a copy of the License at
//
//       http://www.apache.org/licenses/LICENSE-2.0
//
//   Unless required by applicable law or agreed to in writing, software
//   distributed under the License is distributed on an "AS IS" BASIS,
//   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//   See the License for the specific language governing permissions and
//   limitations under the License.

package org.eo.geomapper.parser;

import java.awt.Color;
import java.io.File;
import java.util.ArrayList;
import java.util.List;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;

import org.eo.geomapper.db.GeoMapperDBImporter;
import org.eo.geomapper.log.GeoMapperLogger;
import org.eo.geomapper.objects.GeoLine;
import org.eo.geomapper.objects.GeoPoint;
import org.eo.geomapper.objects.GeoPolygon;
import org.eo.geomapper.objects.ILayer;
import org.eo.geomapper.objects.IObject;
import org.eo.geomapper.objects.ObjectType;
import org.eo.geomapper.objects.SimpleLayer;
import org.eo.geomapper.validator.GeoMapperValidator;
import org.w3c.dom.Document;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;

/**
 * The GeoMapper parser. It parses XML file to recover all informations provided by
 * user
 * 
 * @author Elian ORIOU
 * 
 */

public class GeoMapperParser {

	private List<ILayer> layers;

	private GeoMapperDBImporter dbImporter;

	private String dbHost;
	private String dbUsername;
	private String dbPassword;
	private String dbName;

	public GeoMapperParser() {
		layers = new ArrayList<ILayer>();
	}

	/**
	 * Parses the xml file and returns a list of layers to render
	 * 
	 * @param filePath
	 *            The XML file path
	 * @return A list of layers to render
	 * @throws Exception
	 */

	public List<ILayer> parse(String filePath) throws Exception {

		File file = new File(filePath);

		GeoMapperValidator validator = new GeoMapperValidator();
		if (!validator.validate(file)) {
			throw new Exception(
					"The XML file is not properly structured or contains errors !");
		}

		DocumentBuilder db = DocumentBuilderFactory.newInstance()
				.newDocumentBuilder();
		Document doc = db.parse(file);
		NodeList layersNodes = doc
				.getElementsByTagName(TagNamesProvider.LAYER_TAG_NAME);

		/* Geographical Objects Building */
		for (int i = 0; i < layersNodes.getLength(); i++) {
			Node layer = layersNodes.item(i);
			ILayer newLayer = buildLayer(layer);
			layers.add(newLayer);
			NodeList geoObjectsNodes = layer.getChildNodes();
			for (int j = 0; j < geoObjectsNodes.getLength(); j++) {
				Node element = geoObjectsNodes.item(j);
				IObject newObject = getObject(newLayer, element);
				if (newObject == null) {
					continue;
				}
				newLayer.addObject(newObject);
			}
		}

		/* Configuration Parsing (Render Order, DB Import,...) */
		NodeList configurationNodes = doc
				.getElementsByTagName(TagNamesProvider.CONFIGURATION_TAG_NAME);
		if (configurationNodes.getLength() == 0) {
			GeoMapperLogger.notice("Default Configuration Applied");
			return layers;
		}
		Node dbConnectionNode = configurationNodes.item(0).getChildNodes()
				.item(1);
		getDBConnectionInfos(dbConnectionNode);

		Node loadedLayersNode = configurationNodes.item(0).getChildNodes()
				.item(3);
		layers.addAll(getLoadedLayers(loadedLayersNode));

		return layers;
	}

	/**
	 * Builds a layer from a DOM node
	 * 
	 * @param n
	 *            The DOM node
	 * @return a layer
	 */

	private ILayer buildLayer(Node n) {
		if (n == null) {
			return null;
		}
		String name = n.getAttributes().getNamedItem(
				TagNamesProvider.NAME_ATTR_NAME).getTextContent();
		String type = n.getAttributes().getNamedItem(
				TagNamesProvider.TYPE_ATTR_NAME).getTextContent();

		/* A color set by default to black if not specified */
		Color color = Color.BLACK;
		if (n.getAttributes().getNamedItem(TagNamesProvider.COLOR_ATTR_NAME) != null) {
			color = getColor(n.getAttributes().getNamedItem(
					TagNamesProvider.COLOR_ATTR_NAME).getTextContent());
		}

		if (type.equalsIgnoreCase(TagNamesProvider.POINT_TAG_NAME)) {
			return new SimpleLayer(name, ObjectType.POINT, color);
		} else if (type.equalsIgnoreCase(TagNamesProvider.LINE_TAG_NAME)) {
			return new SimpleLayer(name, ObjectType.LINE, color);
		} else if (type.equalsIgnoreCase(TagNamesProvider.POLYGON_TAG_NAME)) {
			return new SimpleLayer(name, ObjectType.POLYGON, color);
		}
		return null;
	}

	/**
	 * Gets elements in a layer
	 * 
	 * @param l
	 *            The layer to fill
	 * @param node
	 *            The node to analyse
	 * @return an object
	 * @throws Exception
	 */

	private IObject getObject(ILayer layer, Node node) throws Exception {
		if (node == null || node.getAttributes() == null) {
			return null;
		}
		String name = node.getAttributes().getNamedItem(
				TagNamesProvider.NAME_ATTR_NAME).getTextContent();
		switch (layer.getObjectType()) {
		case POINT:
			/* Attempting to have one point */
			Node point = node.getChildNodes().item(1);
			if (!point.getNodeName().equalsIgnoreCase(
					TagNamesProvider.POINT_TAG_NAME)
					|| point.getAttributes() == null) {
				return null;
			}
			int x = Integer.parseInt(point.getAttributes().getNamedItem(
					TagNamesProvider.X_COORD_ATTR_NAME).getTextContent());
			int y = Integer.parseInt(point.getAttributes().getNamedItem(
					TagNamesProvider.Y_COORD_ATTR_NAME).getTextContent());
			return new GeoPoint(name, x, y);
		case LINE:
			/* Attempting to have 2 points at least */
			NodeList linePoints = node.getChildNodes();
			GeoLine line = new GeoLine(name);
			for (int i = 0; i < linePoints.getLength(); i++) {
				Node linePoint = linePoints.item(i);
				if (!linePoint.getNodeName().equalsIgnoreCase(
						TagNamesProvider.POINT_TAG_NAME)
						|| linePoint.getAttributes() == null) {
					continue;
				}
				x = Integer.parseInt(linePoint.getAttributes().getNamedItem(
						TagNamesProvider.X_COORD_ATTR_NAME).getTextContent());
				y = Integer.parseInt(linePoint.getAttributes().getNamedItem(
						TagNamesProvider.Y_COORD_ATTR_NAME).getTextContent());
				GeoPoint p = new GeoPoint(x, y);
				line.addPoint(p);
			}
			return line;
		case POLYGON:
			/* Attempting to have an closed ring with at least 3 points */
			NodeList polygonPoints = node.getChildNodes();
			GeoPolygon polygon = new GeoPolygon(name);
			for (int i = 0; i < polygonPoints.getLength(); i++) {
				Node polygonPoint = polygonPoints.item(i);
				if (!polygonPoint.getNodeName().equalsIgnoreCase(
						TagNamesProvider.POINT_TAG_NAME)
						|| polygonPoint.getAttributes() == null) {
					continue;
				}
				x = Integer.parseInt(polygonPoint.getAttributes().getNamedItem(
						TagNamesProvider.X_COORD_ATTR_NAME).getTextContent());
				y = Integer.parseInt(polygonPoint.getAttributes().getNamedItem(
						TagNamesProvider.Y_COORD_ATTR_NAME).getTextContent());
				GeoPoint p = new GeoPoint(x, y);
				polygon.addPoint(p);
			}
			if (!polygon.getPoints().get(0).equals(
					polygon.getPoints().get(polygon.getLength() - 1))) {
				throw new Exception("The polygon must be an enclosed ring !");
			}
			return polygon;
		}
		return null;
	}

	/**
	 * Recovers DB connection informations provided by user, and tries to
	 * connect to the database
	 * 
	 * @param node
	 */

	private void getDBConnectionInfos(Node node) {

		if (node == null) {
			return;
		}

		dbHost = node.getAttributes().getNamedItem(
				TagNamesProvider.DB_HOST_ATTR_NAME).getTextContent();
		dbUsername = node.getAttributes().getNamedItem(
				TagNamesProvider.DB_USERNAME_ATTR_NAME).getTextContent();
		dbPassword = node.getAttributes().getNamedItem(
				TagNamesProvider.DB_PASSWORD_ATTR_NAME).getTextContent();
		dbName = node.getAttributes().getNamedItem(
				TagNamesProvider.DB_DBNAME_ATTR_NAME).getTextContent();

		String jdbcURL = "jdbc:postgresql://" + dbHost + "/" + dbName;

		dbImporter = new GeoMapperDBImporter();
		dbImporter.connect(jdbcURL, dbUsername, dbPassword);
	}

	/**
	 * Returns the loaded layers provided by user in the XML file
	 * 
	 * @param node
	 * @return
	 */

	private List<ILayer> getLoadedLayers(Node node) {
		if (node == null) {
			return null;
		}

		List<ILayer> layers = new ArrayList<ILayer>();
		for (int i = 0; i < node.getChildNodes().getLength(); i++) {
			Node currentNode = node.getChildNodes().item(i);
			if (!currentNode.getNodeName().equals(
					TagNamesProvider.LOADED_LAYER_TAG_NAME)) {
				continue;
			}
			String layerType = currentNode.getAttributes().getNamedItem(
					TagNamesProvider.TYPE_ATTR_NAME).getTextContent();
			String tableName = currentNode.getAttributes().getNamedItem(
					TagNamesProvider.TABLENAME_ATTR_NAME).getTextContent();
			String nameField = currentNode.getAttributes().getNamedItem(
					TagNamesProvider.NAMEFIELD_ATTR_NAME).getTextContent();
			String coordField = currentNode.getAttributes().getNamedItem(
					TagNamesProvider.COORDFIELD_ATTR_NAME).getTextContent();

			layers.add(dbImporter.importLayer(ObjectType
					.getTypeFromString(layerType), tableName, nameField,
					coordField));
		}
		return layers;
	}

	/**
	 * Returns a color by its name (red, blue, green, etc...)
	 * 
	 * @param colorName
	 * @return
	 */

	private Color getColor(String colorName) {
		if (colorName.equals("black")) {
			return Color.BLACK;
		} else if (colorName.equals("blue")) {
			return Color.BLUE;
		} else if (colorName.equals("red")) {
			return Color.RED;
		} else if (colorName.equals("gray")) {
			return Color.DARK_GRAY;
		} else if (colorName.equals("pink")) {
			return Color.PINK;
		} else if (colorName.equals("green")) {
			return Color.GREEN;
		} else if (colorName.equals("cyan")) {
			return Color.CYAN;
		} else if (colorName.equals("magenta")) {
			return Color.MAGENTA;
		} else if (colorName.equals("orange")) {
			return Color.ORANGE;
		} else if (colorName.equals("yellow")) {
			return Color.YELLOW;
		}
		return null;
	}
}
